﻿using System.Windows.Forms;
using System;
using TripleGame.GUI.Properties;

namespace TripleGame.GUI
{
    public partial class FormTripleGame : Form
    {
        private const int BOARD_COLUMNS_COUNT = 10;
        private const int BOARD_ROWS_COUNT = 5;
        private const char EMPTY_CELL = ' ';
        private const int TURN_TIMEOUT_MILLISECONDS = 1000000;

        private char[,] board;

        private Player[] players;
        private int activePlayerIndex;

        public FormTripleGame()
        {
            InitializeComponent();
            InitializeBoard();
            InitializePlayers();
            InitializeGameEngines();
        }

        private void InitializeBoard()
        {
            this.board = new char[BOARD_ROWS_COUNT, BOARD_COLUMNS_COUNT];
            for (int row = 0; row < BOARD_ROWS_COUNT; row++)
            {
                for (int col = 0; col < BOARD_COLUMNS_COUNT; col++)
                {
                    this.board[row, col] = EMPTY_CELL;
                }
            }
            RedrawBoard();
        }

        private void InitializePlayers()
        {
            this.players = new Player[]
            { 
                new Player() { Letter = 'A' }, 
                new Player() { Letter = 'B' }
            };
        }

        private void InitializeGameEngines()
        {
            if (!String.IsNullOrEmpty(Settings.Default.GameEngineA))
            {
                LoadPlayerEngine(players[0], Settings.Default.GameEngineA, labelEngineA);
            }
            if (!String.IsNullOrEmpty(Settings.Default.GameEngineB))
            {
                LoadPlayerEngine(players[1], Settings.Default.GameEngineB, labelEngineB);
            }
        }

        private void RedrawBoard()
        {
            customControlBoard.Board = this.board;
        }

        private void LogMessage(string msg)
        {
            this.textBoxLog.AppendText(msg);
            this.textBoxLog.AppendText("\n");
            this.textBoxLog.SelectionStart = this.textBoxLog.Text.Length;
            this.textBoxLog.ScrollToCaret();
        }

        private void ShowGameState(string msg)
        {
            this.textBoxGameState.Text = msg;
        }

        private void buttonNewGame_Click(object sender, System.EventArgs e)
        {
            InitializeBoard();
            if (players[0].Engine != null || players[1].Engine != null)
            {
                LogMessage("New game started.");
                this.activePlayerIndex = 0;
                this.players[0].Score = 0;
                this.players[1].Score = 0;
                this.buttonNextMove.Enabled = true;
                PerformNextMove();
            }
            else
            {
                LogMessage("Error: Game engines are not loaded. Cannot start game.");
            }
        }

        private void buttonLoadEngineA_Click(object sender, System.EventArgs e)
        {
            if (openFileDialogEngine.ShowDialog() == DialogResult.OK)
            {
                Player player = this.players[0];
                if (LoadPlayerEngine(player, openFileDialogEngine.FileName, labelEngineA))
                {
                    Settings.Default.GameEngineA = openFileDialogEngine.FileName;
                    Settings.Default.Save();
                }
            }
        }

        private void buttonLoadEngineB_Click(object sender, EventArgs e)
        {
            if (openFileDialogEngine.ShowDialog() == DialogResult.OK)
            {
                Player player = this.players[1];
                if (LoadPlayerEngine(player, openFileDialogEngine.FileName, labelEngineB))
                {
                    Settings.Default.GameEngineB = openFileDialogEngine.FileName;
                    Settings.Default.Save();
                }
            }
        }

        private bool LoadPlayerEngine(Player player, string engineFileName, Label labelEngine)
        {
            try
            {
                player.LoadEngine(engineFileName);
                this.LogMessage("Engine '" + player.Engine.EngineName + 
                    "' loaded for player " + player.Letter + ".");
                labelEngine.Text = player.Engine.EngineName;
                return true;
            }
            catch (Exception ex)
            {
                this.LogMessage("Error: " + ex.Message);
                return false;
            }
        }

        private void buttonNextMove_Click(object sender, EventArgs e)
        {
            PerformNextMove();
        }

        private void PerformNextMove()
        {
            Player playerOnTurn = players[activePlayerIndex];
            ShowGameState("Player " + playerOnTurn.Letter + " on turn.");

            UpcaseAllBoardLetters(this.board);

            // Try to perform a move through the player's engine
            int startX = 0;
            int startY = 0;
            int endX = 0;
            int endY= 0;
            try
            {
                TimeBoundBackgroundExecutor timeBoundExecutor = 
                    new TimeBoundBackgroundExecutor(TURN_TIMEOUT_MILLISECONDS);
                timeBoundExecutor.Execute(delegate()
                {
                    char[,] boardCopy = (char[,])this.board.Clone();
                    playerOnTurn.Engine.PerformMove(
                        boardCopy, playerOnTurn.Letter, out startX, out startY, out endX, out endY);
                });
            }
            catch (Exception ex)
            {
                // Exception in the player's engine -> show error and lose the game
                LogMessage(String.Format("Player {0} is trying to move...", playerOnTurn.Letter));
                LogMessage("Error in the player's engine: " + ex.Message);
                GameLost(playerOnTurn);
                return;
            }

            // Process the player's move
            try
            {
                ProcessMove(playerOnTurn, startX, startY, endX, endY);
                LogMessage(String.Format("Player {0} moved: [{1},{2}]-[{3},{4}]",
                    playerOnTurn.Letter, startX, startY, endX, endY));
            }
            catch (Exception ex)
            {
                // The player's move was illegal -> show error and lose the game
                LogMessage(String.Format("Player {0} is trying to move: [{1},{2}]-[{3},{4}]",
                    playerOnTurn.Letter, startX, startY, endX, endY));
                LogMessage("Error: " + ex.Message);
                GameLost(playerOnTurn);
                return;
            }

            // Display the board after the current player's move
            RedrawBoard();
            UpdatePlayersScores();

            // The next player becomes on move
            activePlayerIndex = 1 - activePlayerIndex;

            // Check for game over
            if (!HasValidMove(activePlayerIndex))
            {
                // The next player becomes on move
                activePlayerIndex = 1 - activePlayerIndex;
                if (!HasValidMove(activePlayerIndex))
                {
                    GameOver();
                }
            }
        }

        private void UpcaseAllBoardLetters(char[,] board)
        {
            for (int row = 0; row < BOARD_ROWS_COUNT; row++)
            {
                for (int col = 0; col < BOARD_COLUMNS_COUNT; col++)
                {
                    board[row, col] = Char.ToUpperInvariant(board[row, col]);
                }
            }            
        }

        private bool HasValidMove(int playerIndex)
        {
            Player player = this.players[playerIndex];
            for (int row = 0; row < BOARD_ROWS_COUNT; row++)
            {
                for (int col = 0; col < BOARD_COLUMNS_COUNT; col++)
                {
                    bool rightMoveValid = IsMoveValid(player, row, col, 1, 0);
                    bool downMoveValid = IsMoveValid(player, row, col, 0, 1);
                    bool downRightMoveValid = IsMoveValid(player, row, col, 1, 1);
                    if (rightMoveValid || downMoveValid || downRightMoveValid)
                    {
                        return true;
                    }
                }
            }
            
            // No valid moves were found anywhere at the board
            return false;
        }

        private bool IsMoveValid(Player player, int startRow, int startCol, int stepX, int stepY)
        {
            int row = startRow;
            int col = startCol;
            int numberOfEmptyCellsFilled = 0;
            for (int i = 1; i <= 3; i++)
            {
                if (row < 0 || row >= BOARD_ROWS_COUNT ||
                    col < 0 || col >= BOARD_COLUMNS_COUNT)
                {
                    return false;
                }

                char currentCellUpcase = Char.ToUpperInvariant(board[row, col]);
                char playerLetterUpcase = Char.ToUpperInvariant(player.Letter);
                if (currentCellUpcase == EMPTY_CELL)
                {
                    numberOfEmptyCellsFilled++;
                }
                else if (currentCellUpcase != playerLetterUpcase)
                {
                    return false;
                }
                row += stepY;
                col += stepX;
            }

            if (numberOfEmptyCellsFilled == 0)
            {
                return false;
            }

            return true;
        }

        private void GameLost(Player player)
        {
            string gameOverText;
            if (player == null)
            {
                gameOverText = "Game over but nobody wins.";
            }
            else
            {
                gameOverText = "Player " + player.Letter + " lost!";
            }
            ShowGameState(gameOverText);
            LogMessage(gameOverText);
            this.buttonNextMove.Enabled = false;
        }

        private void GameOver()
        {
            if (players[0].Score > players[1].Score)
            {
                GameLost(players[1]);
            }
            else if (players[0].Score < players[1].Score)
            {
                GameLost(players[0]);
            }
            else
            {
                GameLost(null);
            }
        }

        private void ProcessMove(
            Player player, int startX, int startY, int endX, int endY)
        {
            // Check wheather the specified block is inside the board
            if (startX < 0 || startX >= BOARD_COLUMNS_COUNT ||
                startY < 0 || startY >= BOARD_ROWS_COUNT ||
                endX < 0 || endX >= BOARD_COLUMNS_COUNT ||
                endY < 0 || endY >= BOARD_ROWS_COUNT)
            {
                throw new ArgumentOutOfRangeException("The move is outside of the board!");
            }

            // Check wheather the specified block is of size [3 x 1]
            int sizeX = Math.Abs(startX - endX) + 1;
            int sizeY = Math.Abs(startY - endY) + 1;
            bool correctShape =
                (sizeX == 3 && sizeY == 1) ||
                (sizeX == 1 && sizeY == 3) ||
                (sizeX == 3 && sizeY == 3);
            if (!correctShape)
            {
                throw new ArgumentOutOfRangeException("The move should be block of size 3 x 1!");
            }

            // Check wheather the specified block overlaps only allowed cells
            int stepX = Math.Sign(endX - startX);
            int stepY = Math.Sign(endY - startY);
            int row = startY;
            int col = startX;
            int numberOfEmptyCellsFilled = 0;
            for (int i = 1; i <= 3; i++)
            {
                char currentCellUpcase = Char.ToUpperInvariant(board[row, col]);
                char playerLetterUpcase = Char.ToUpperInvariant(player.Letter);
                if (currentCellUpcase == EMPTY_CELL)
                {
                    this.board[row, col] = Char.ToLowerInvariant(player.Letter);
                    numberOfEmptyCellsFilled++;
                }
                else if (currentCellUpcase == playerLetterUpcase)
                {
                    this.board[row, col] = Char.ToLowerInvariant(player.Letter);
                }
                else
                {
                    throw new InvalidOperationException(
                        "Attempt to overlap cells occupied by the other player.");
                }
                row += stepY;
                col += stepX;
            }

            if (numberOfEmptyCellsFilled == 0)
            {
                throw new InvalidOperationException(
                    "The move should fill at least one empty cell.");
            }

            player.Score += numberOfEmptyCellsFilled;
        }

        private void UpdatePlayersScores()
        {
            this.labelScoreA.Text = this.players[0].Score.ToString();
            this.labelScoreB.Text = this.players[1].Score.ToString();
        }
    }
}
